#!/usr/bin/perl -w

BEGIN   
{
	sub getPoints{return '../../' if (!$_[1]);return '../' if (!$_[0]);return '/';}
	(my $file = __FILE__) =~ s/\/?(modules\/)?(http_ip_limit?\/)?(\w+\.pl)$/&getPoints($1,$2)/e;
	$file = './' if $file eq '/';
	unshift(@INC, $file."bin");
	unshift(@INC, $file.'modules/http_bandwidth');
	unshift(@INC, $file.'modules/http_ip_limit');
	unshift(@INC, $file.'modules/http_logging');
}


use strict;
use Data::Dumper;
use TWM;
use Bandwidth;
use IPLimit;
use Logging;
use POSIX ":sys_wait_h";
use IPC::ShareLite;
use Storable qw( freeze thaw );


my $HAS_POSIX = 1;


my ($handle_blocked_ip_list, $handle_wait_main, $data_shared);
my (%maxsize, $access_log, $pid);


my $debug_level = $ck{debug_level};
my $objIPLimit = IPLimit->new();
my $objBandWidth = Bandwidth->new();
my $logging = new Logging;
my $chld_id = 'undefined';
my $is_main_child = 1;

my ($web_string, $ip_string) = ('', '');
populateCfromConfig($objIPLimit->{http_bandwidth_code});
populateCfromConfig($objIPLimit->{http_logging_code});
populateCfromConfig($logging->{this_module_code});

my $squid_settings = $logging->getSquidsSettings();
if (!$squid_settings)
{
	logModuleError('No squid settings are defined in the \'HTTP Logs\' configuration');
	exit;
}

my $old_child_handler = $SIG{CHLD};
my %child_process = ();

$SIG{INT} = \&got_int;
$SIG{CHLD} = \&reaper;
$SIG{USR1} = sub
{
	toDebugLog("Got USR1");
	&init;
};
$SIG{USR2} = sub
{
	toDebugLog("Got USR2");
	&clear;
};

#$SIG{TERM} = \&got_int;		#-15
#$SIG{KILL} = \&got_int;	#-9
#$SIG{QUIT} = \&got_int;		#-3
#$SIG{STOP} = \&got_int;		#-19


my $handle_total = IPC::ShareLite->new(
        -key     => 1975,
        -create  => 'yes',
        -destroy => 'no'
    ) or die $!;


# if daemon has beed started today already then need load log otherwise need clear log
my $mtime = $data_shared->{mtime} || time();
store($data_shared);
my ($sec_mtime, $min_mtime, $hour_mtime, $day_mtime, $month_mtime, $year_mtime) = (localtime($mtime))[0,1,2,3,4,5];
my ($sec, $min, $hour, $day, $month, $year) = (localtime)[0,1,2,3,4,5];
$day_mtime == $day && $month_mtime == $month && $year_mtime == $year ? &init : &clear;

foreach my $v (values %$squid_settings)
{
	$access_log = $v->{log_folder}.'/'.$v->{access_log};
	if (! -e $access_log)
	{
		toErrorLog("Could not find file : $access_log");
		next;
	}
	$chld_id = $v->{port};
	$pid = $cm{pid}."_".$chld_id;

	my $child_pid = fork();
	die "Unable to fork : $!\n" if (!defined($child_pid)); 
	if ($child_pid) 
	{
		toDebugLog("Child pid: $child_pid , port $chld_id");
		open(PID, ">$pid") or die "Unable to create pid file $pid: $!";
		print PID "$child_pid\n";
		close PID;
	}
	else
	{
		toDebugLog("[$chld_id : $$] Child started with is_main_child=$is_main_child");
		toDebugLog("[$chld_id : $$] Closing child STDIN STDOUT STDERR");
		close STDIN;
		open(STDIN, '</dev/null');
		close STDOUT;
		open(STDOUT, '>/dev/null');
		close STDERR;
		open(STDERR, '>/dev/null');
		&processLogData;
		toDebugLog("[$chld_id : $$] Child has finished.");
		exit(0);
	}
	$is_main_child = 0;
}
toDebugLog("Close parent");
exit(0);






sub processLogData  
{
	toDebugLog("[$chld_id : $$] Open log file: $access_log");
	open(LOG_FILE,"tail --follow=name --retry --pid=$$ $access_log 2>/dev/null |") or die "Unable to open log file $access_log : $!\n";
	toDebugLog("[$chld_id : $$] Processing log data");
	DO_WHILE: while (<LOG_FILE>)  
	{
		if (/(\d+\.\d+)\s+(\d+)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(.+?)\s+(\d+)\s+(\w+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)/) 
		{
			my ($time, $duration, $client_address, $result_codes, $bytes, $request_method, $url, $rfc931, $hierarchy_code, $type) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
			# ignore if 5 seconds elapsed (there is some strange but sometimes tail returns old rows)
			next if (time() - $time > 5);
			my ($ip, $size) = ($client_address, $bytes);

			$url = $1 if ($url =~ /https?:\/\/(.+?)\//);
			next if ($url =~ /$web_string/);
			next if ($ip =~ /$ip_string/);
			$handle_total->lock();
			my $data_shared = fetch();
			$data_shared->{total}->{$ip} += $size;
			$data_shared->{mtime} = time();
			if ($debug_level > 4)
			{
				my $d = time() - $time;
				$time =~ s/^\d+\.\d+/localtime $&/e;
				toDebugLog("[$chld_id : $$] $d, $time, $ip, $size, $url, total: $data_shared->{total}->{$ip}");
			}
			if (!exists($data_shared->{blocked_ip_list}->{$ip}) && exists($maxsize{$ip}) && $maxsize{$ip} < $data_shared->{total}->{$ip})
			{
				$data_shared->{blocked_ip_list}->{$ip} = $data_shared->{total}->{$ip};
				$objIPLimit->saveBlockedList($data_shared->{blocked_ip_list});
				($debug_level > 2) && toDebugLog("[$chld_id : $$] $ip is blocked: max=$maxsize{$ip}  total=$data_shared->{total}->{$ip}\nReconfigured squid.");
			}
			elsif (exists($data_shared->{blocked_ip_list}->{$ip}) && exists($maxsize{$ip}) && $maxsize{$ip} > $data_shared->{total}->{$ip})
			{
				($debug_level > 2) && toDebugLog("[$chld_id : $$] $ip removed from blocked list: max=$maxsize{$ip}  total=$data_shared->{total}->{$ip}\nReconfigured squid.".Dumper($data_shared->{blocked_ip_list}));
				delete $data_shared->{blocked_ip_list}->{$ip};
				$objIPLimit->saveBlockedList($data_shared->{blocked_ip_list});
			}
			store($data_shared);
			$handle_total->unlock();
		}
	}
}


sub get_reset_ip
{
	if (-e "$ck{twmfolder}/modules/$mc/logs/$cm{reset_ip}")
	{
		open(FL, "$ck{twmfolder}/modules/$mc/logs/$cm{reset_ip}");
		my $ip = join("",<FL>);
		close(FL);
		unlink "$ck{twmfolder}/modules/$mc/logs/$cm{reset_ip}";
		$ip;
	}
}


sub clear
{
	if ($is_main_child)
	{
		my $data_shared = fetch();
		$data_shared->{wait_main} = 1;
# main child clears all shared data
		toDebugLog("Clear log file with child $chld_id.");
		my $ip = &get_reset_ip;
		if ($ip)
		{
			delete $data_shared->{total}->{$ip} if (exists($data_shared->{total}->{$ip}));
			delete $data_shared->{blocked_ip_list}->{$ip} if (exists($data_shared->{blocked_ip_list}->{$ip}));
			
		}
		else
		{
			$data_shared->{total} = {};
			$data_shared->{blocked_ip_list} = {};
		}
		$objIPLimit->saveBlockedList($data_shared->{blocked_ip_list});
		$data_shared->{wait_main} = 0;
		store($data_shared);
	}
	else
	{
# not main children should wait
		sleep (1);
		while(fetch()->{wait_main})
		{
			sleep(1);
		}
	}
	&init;
}


sub got_int 
{
	toDebugLog("Got SIGINT with child $chld_id");
	unlink $pid;
	&closeLogData;
	$SIG{INT} = \&got_int;
	exit 0;
}


sub init
{
	toDebugLog("[$chld_id : $$] Initialization:");
	&set_custom_restrictions;
	&set_web_string;
	if ($is_main_child)
	{
# main child init shared data
		my $data_shared = fetch();
		$data_shared->{wait_main} = 1;
		$data_shared->{blocked_ip_list} = {};
		toDebugLog("[$chld_id : $$] Read blocked ip");
		my $str = '';
		my $ip_list = $objIPLimit->get_blocked_list();
		foreach (@$ip_list)
		{
			next if !$_;
			$data_shared->{blocked_ip_list}->{$_} = 1;
			$str .= "	$_ is blocked\n";
		}
		toDebugLog($str||"[$chld_id : $$] There is not any new blocked ip");
		$str = '';
        
		toDebugLog("[$chld_id : $$] Read log file to add new blocked ip.");
		my $is_new_blocked = 0;
		for my $ip (keys %maxsize)  
		{
			if (!exists($data_shared->{blocked_ip_list}->{$ip}) && exists($maxsize{$ip}) && exists($data_shared->{total}->{$ip}) && $maxsize{$ip} <= $data_shared->{total}->{$ip})
			{
				$is_new_blocked = 1;
				$data_shared->{blocked_ip_list}->{$ip} = $data_shared->{total}->{$ip};
				$str .= "	$ip is blocked: max=$maxsize{$ip}  total=$data_shared->{total}->{$ip}\n";
			}
		}
		toDebugLog($str||"[$chld_id : $$] There is not any new ip to block");
		$is_new_blocked && $objIPLimit->saveBlockedList($data_shared->{blocked_ip_list});
		$data_shared->{wait_main} = 0;
		store($data_shared);
		toDebugLog(Dumper $data_shared);
	}
	else
	{
# not main children should wait
		sleep (1);
		while(fetch()->{wait_main})
		{
			toDebugLog("[$chld_id : $$] is waiting");
			sleep(1);
		}
	}
}


###########################################################
# Set default restriction for all ip
###########################################################
sub set_default_restrictions  
{
	%maxsize = ();
	my $ip_list = &getIPs;
	my $default_size = $cm{default_amount}*1024*1024;
	$maxsize{$_->{ip}} = $default_size for (@$ip_list);
	toDebugLog("[$chld_id : $$] Set default size $default_size for all ip");
}


###########################################################
# Set custom restriction for all ip
###########################################################
sub set_custom_restrictions
{
	&set_default_restrictions;
	my $str = "[$chld_id : $$] Set custom restrictions:\n";
	my $xml = &readModuleConfigFile($cm{restriction_file});
	my $areas = $xml->{'pc'};
	if (ref $areas eq 'HASH')
	{
		$maxsize{$areas->{ip}} = $areas->{size}*1024*1024;
		$str .= "	$areas->{ip} new size: $maxsize{$areas->{ip}}\n";
	}
	else	
	{
		foreach (@$areas)
		{
			$maxsize{$_->{ip}} = $_->{size}*1024*1024;
			$str .= "	$_->{ip} new size: $maxsize{$_->{ip}}\n";
		}
	}
	toDebugLog($str);
}



###########################################################
# Set web/ip strings which will ignored
###########################################################
sub set_web_string  
{
	($web_string, $ip_string) = ('')x2;
	my $xml = readModuleConfigFile($cm{ignore_classes});
	my $classes_config = &readModuleConfigFile($C->{$objIPLimit->{http_bandwidth_code}}->{classes_config}, $objIPLimit->{http_bandwidth_code});
	my @list;
	if (ref $xml->{acl}->{name} eq 'ARRAY')
	{
		for (@{$xml->{acl}->{name}})  
		{
			my $acl = $objBandWidth->getACL($_, $xml);
			push @list, $objBandWidth->parseData([split(/\n/, (ref $acl->{'content'} ne 'HASH')?$acl->{'content'}:'')]) if $acl;
		}
	}
	else
	{
		my $class_acl = $objBandWidth->getACL($xml->{acl}->{name}, $classes_config);
		push @list, $objBandWidth->parseData([split(/\n/, (defined $class_acl && ref $class_acl->{'content'} ne 'HASH')?$class_acl->{'content'}:'')]);
	}

	for (@list)
	{
		if ($_->{fld} =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)
		{
			$ip_string.="|$_->{fld}";
		}
		else
		{
			$web_string .= "|$_->{fld}";
		}
	}
	if ($web_string)
	{
		$web_string =~ s/^\|//;
		$web_string = "\^($web_string)\$";
	}
	if ($ip_string)
	{
		$ip_string =~ s/^\|//;
		$ip_string = "\^($ip_string)\$";
	}
	($web_string || $ip_string) && toDebugLog("[$chld_id : $$] Ignored data:\n$web_string\n$ip_string");
}


sub closeLogData  
{
	toDebugLog("[$chld_id : $$] Close log file: $access_log");
#	close(LOG_FILE) or toErrorLog("[$chld_id : $$] Can not close log file $access_log: $!");
}


#############################################################
# check died children
#############################################################
sub reaper
{
	toDebugLog("[$chld_id : $$] Reaper @_");
	if ($HAS_POSIX)
	{
		foreach my $pid (keys %child_process) 
		{
			my $res = $HAS_POSIX ? waitpid($pid, WNOHANG) : waitpid($pid,0);
			if ($res > 0) 
			{
				delete $child_process{$pid};
				toDebugLog("[$chld_id : $$] Removed \$pid=$pid");
			}
		}
	} 
	else
	{
		my $waitedpid = 0;
		while ($waitedpid != -1) 
		{
			$waitedpid = wait;
		}
	}

        if ($old_child_handler && ref $old_child_handler eq 'CODE')
        {
            &$old_child_handler;
        }
}

sub got_SIG
{
  my $signal = shift;
  toDebugLog("[$chld_id : $$] Got [$signal] signal. Ignore!");
}

sub store
{
	$handle_total->store(freeze shift);
}

sub fetch
{
	return thaw($handle_total->fetch());
}
